package httpclient

import (
	"bytes"
	"context"
	"crypto/tls"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"mime/multipart"
	"net"
	"net/http"
	"net/http/cookiejar"
	"path/filepath"
	"strings"
	"sync"
	"time"
)

const clientVersion = "0.0.3"

type ClientHttp struct {
	params            map[string]interface{}
	requestBody       []byte
	files             []uploadFile
	host              string
	header            http.Header
	contentType       string
	errorRaw          string
	timeout           int
	mutex             sync.Mutex
	cooJar            *cookiejar.Jar
	skipHttps         bool
	disableKeepAlives bool

	cookieData map[string]*http.Cookie
}

type ClientResp struct {
	Body        []byte // response body
	BodyRaw     *http.Response
	RequestTime time.Duration
	CookieData  map[string]*http.Cookie
}

type uploadFile struct {
	// 表单名称
	Name string
	// 文件路径
	Filepath string
}

var DefaultTransport = &http.Transport{
	Proxy: http.ProxyFromEnvironment,
	DialContext: (&net.Dialer{
		Timeout:   30 * time.Second,
		KeepAlive: 30 * time.Second,
		DualStack: true,
	}).DialContext,
	ForceAttemptHTTP2:     true,
	MaxIdleConns:          100,
	IdleConnTimeout:       90 * time.Second,
	TLSHandshakeTimeout:   10 * time.Second,
	ExpectContinueTimeout: 1 * time.Second,
}

func (c *ClientResp) String() string {
	return string(c.Body)
}

func (c *ClientResp) GetRequestTime() time.Duration {
	return c.RequestTime
}

func NewClient() *ClientHttp {
	defaultHeader := make(http.Header)
	defaultHeader.Add("User-Agent", "OrangeClient/"+clientVersion)
	return &ClientHttp{
		contentType: "application/x-www-form-urlencoded",
		header:      defaultHeader,
		timeout:     10,
		errorRaw:    "",
		files:       make([]uploadFile, 0),
		cookieData:  make(map[string]*http.Cookie),
	}
}

// Header 原样保持header头大小写设置
func (c *ClientHttp) Header(key, value string) *ClientHttp {
	c.mutex.Lock()
	defer c.mutex.Unlock()
	c.header[key] = []string{value}
	if strings.ToLower(key) == "content-type" {
		c.contentType = value
	}
	return c
}

// HeaderSet 自动校正header头格式
func (c *ClientHttp) RequestHeader() http.Header {
	return c.header
}

// HeaderSet 自动校正header头格式
func (c *ClientHttp) HeaderSet(key, value string) *ClientHttp {
	c.mutex.Lock()
	defer c.mutex.Unlock()
	c.header.Set(key, value)
	if strings.ToLower(key) == "content-type" {
		c.contentType = value
	}
	return c
}

func (c *ClientHttp) ContentType(value string) *ClientHttp {
	c.mutex.Lock()
	defer c.mutex.Unlock()
	c.header.Set("Content-Type", value)
	c.contentType = value

	return c
}

// WithHost 添加请求头host
func (c *ClientHttp) WithHost(host string) *ClientHttp {
	c.host = host
	return c
}

// WithFile 添加文件form-data方式发送
func (c *ClientHttp) WithFile(name string, filePath string) *ClientHttp {
	c.files = append(c.files, uploadFile{name, filePath})
	return c
}

// SkipHttps 跳过https证书校验
func (c *ClientHttp) SkipHttps() *ClientHttp {
	c.skipHttps = true
	return c
}

// DisableKeepAlives 关闭KeepAlives
func (c *ClientHttp) DisableKeepAlives() *ClientHttp {
	c.disableKeepAlives = true
	return c
}

// ResetParam 清空重置请求参数
func (c *ClientHttp) ResetParam() *ClientHttp {
	c.requestBody = []byte{}
	c.params = map[string]interface{}{}
	return c
}

// WithFormRequest 快速配置表单请求类型
func (c *ClientHttp) WithFormRequest() *ClientHttp {
	c.mutex.Lock()
	defer c.mutex.Unlock()
	c.header.Set("Content-Type", "application/x-www-form-urlencoded")
	c.contentType = "application/x-www-form-urlencoded"

	return c
}

// WithFormRequest 快速配置表单请求类型
func (c *ClientHttp) WithJsonRequest() *ClientHttp {
	c.mutex.Lock()
	defer c.mutex.Unlock()
	c.header.Set("Content-Type", "application/json")
	c.contentType = "application/json"

	return c
}

// RunGet 执行Get请求
func (c *ClientHttp) RunGet(clientUrl string) (clientResp *ClientResp, err error) {
	clientResp = &ClientResp{
		CookieData: c.cookieData,
	}
	if c.errorRaw != "" {
		return nil, c.Error()
	}

	var defaultClient = &http.Client{}

	if c.cooJar != nil {
		defaultClient.Jar = c.cooJar
	}

	transport := DefaultTransport
	transport.DisableKeepAlives = c.disableKeepAlives

	if c.skipHttps == true {
		transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
	}
	defaultClient.Transport = http.RoundTripper(transport)

	req, err := http.NewRequest("GET", clientUrl, nil)
	if err != nil {
		return clientResp, err
	}
	timeSt := time.Now()
	// 超时设置
	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(c.timeout)*time.Second)
	defer cancel()
	req = req.WithContext(ctx)
	if c.host != "" {
		req.Host = c.host
	}

	//c.header.Set("Cookie", "")
	req.Header = c.header

	resp, err := defaultClient.Do(req)
	if err != nil {
		return clientResp, err
	}
	defer resp.Body.Close()

	for _, item := range resp.Cookies() {
		c.setCookieData(item.Name, item)
	}

	requestTime := time.Now().Sub(timeSt)
	body, err := ioutil.ReadAll(resp.Body)
	resp.Body = ioutil.NopCloser(bytes.NewReader(body))

	clientResp.BodyRaw = resp
	clientResp.Body = body
	clientResp.RequestTime = requestTime

	return clientResp, err
}

func (c *ClientHttp) RunPost(clientUrl string) (clientResp *ClientResp, err error) {
	return c.RunRequest(http.MethodPost, clientUrl)
}

func (c *ClientHttp) RunPatch(clientUrl string) (clientResp *ClientResp, err error) {
	return c.RunRequest(http.MethodPatch, clientUrl)
}

func (c *ClientHttp) RunPut(clientUrl string) (clientResp *ClientResp, err error) {
	return c.RunRequest(http.MethodPut, clientUrl)
}

// RunGet 执行Post请求
func (c *ClientHttp) RunRequest(method, clientUrl string) (clientResp *ClientResp, err error) {
	clientResp = &ClientResp{
		CookieData: c.cookieData,
	}

	if len(c.params) > 0 || len(c.files) > 0 {
		c.paraseParams()
	}

	if c.errorRaw != "" {
		return nil, c.Error()
	}

	var defaultClient = &http.Client{}
	if c.cooJar != nil {
		defaultClient.Jar = c.cooJar
	}

	transport := DefaultTransport
	transport.DisableKeepAlives = c.disableKeepAlives
	if c.skipHttps == true {
		transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
	}
	defaultClient.Transport = http.RoundTripper(transport)

	req, err := http.NewRequest(method, clientUrl, bytes.NewBuffer(c.requestBody))
	if err != nil {
		return clientResp, err
	}

	timeSt := time.Now()
	// 超时设置
	ctx, cancel := context.WithTimeout(context.Background(), time.Duration(c.timeout)*time.Second)
	defer cancel()
	req = req.WithContext(ctx)
	c.header.Set("Cookie", "")
	req.Header = c.header
	req.Header.Set("Content-Type", c.contentType)
	req.Header.Set("Accept", "*")

	if c.host != "" {
		req.Host = c.host
	}

	resp, err := defaultClient.Do(req)
	if err != nil {
		return clientResp, err
	}
	defer resp.Body.Close()

	for _, item := range resp.Cookies() {
		c.setCookieData(item.Name, item)
	}

	requestTime := time.Now().Sub(timeSt)
	body, err := ioutil.ReadAll(resp.Body)
	resp.Body = ioutil.NopCloser(bytes.NewReader(body))

	clientResp.BodyRaw = resp
	clientResp.Body = body
	clientResp.RequestTime = requestTime

	return clientResp, err
}

func (c *ClientHttp) SetCookies(cookies []*http.Cookie) *ClientHttp {
	for _, item := range cookies {
		c.setCookieData(item.Name, item)
	}
	return c
}

func (c *ClientHttp) SetTimeout(timeout int) *ClientHttp {
	c.timeout = timeout
	return c
}

// cookie保持 通过配置请求id将cookie保持
func (c *ClientHttp) WithCookie() *ClientHttp {
	if c.cooJar == nil {
		cooJar, _ := cookiejar.New(nil)
		c.cooJar = cooJar
	}
	return c
}

// 直接传递body中的参数
func (c *ClientHttp) WithBody(bodyStream string) *ClientHttp {
	c.requestBody = []byte(bodyStream)
	return c
}

// 参数设置 表单请求支持json和form两种类型
func (c *ClientHttp) FormParams(obj map[string]interface{}) *ClientHttp {
	c.params = obj
	return c
}

// 将结构体或任意结构转成json作为参数
func (c *ClientHttp) WithJsonBody(data interface{}) *ClientHttp {
	c.mutex.Lock()
	defer c.mutex.Unlock()
	c.header.Set("Content-Type", "application/json")
	c.contentType = "application/json"

	dataJson, _ := json.Marshal(data)
	c.requestBody = dataJson
	return c
}

func (c *ClientHttp) paraseParams() *ClientHttp {
	if len(c.files) > 0 {
		body := &bytes.Buffer{}
		writer := multipart.NewWriter(body)
		for _, uploadFile := range c.files {
			fileBytes, err := ioutil.ReadFile(uploadFile.Filepath)
			if err != nil {
				fmt.Printf("can not open file %s", uploadFile.Filepath)
				continue
			}
			part, err := writer.CreateFormFile(uploadFile.Name, filepath.Base(uploadFile.Filepath))
			if err != nil {
				fmt.Printf("CreateFormFile error %v", err)
				continue
			}
			part.Write(fileBytes)
		}

		for k, v := range c.params {
			stringVal := fmt.Sprintf("%v", v)
			if err := writer.WriteField(k, stringVal); err != nil {
				fmt.Printf("WriteField error %v", err)
				continue
			}
		}

		c.contentType = writer.FormDataContentType()
		if err := writer.Close(); err != nil {
			panic(err)
		}

		c.requestBody = body.Bytes()
		return c
	}

	if strings.Contains(c.contentType, "application/x-www-form-urlencoded") {
		params := ""
		val := make([]interface{}, 0, len(c.params))
		for k, item := range c.params {
			params += k + "=%v&"
			val = append(val, item)
		}
		paramsLen := len(params) - 1
		params = params[0:paramsLen]
		params = fmt.Sprintf(params, val...)
		c.requestBody = []byte(params)

		return c
	}

	if strings.Contains(c.contentType, "application/json") {
		params, err := json.Marshal(c.params)
		if err != nil {
			c.errorRaw += err.Error() + "|"
		}
		c.requestBody = params
		return c
	}
	c.errorRaw += "Use params you  must set  Content-Type eq 'application/json' or 'application/x-www-form-urlencoded'"

	return c
}

func (c *ClientHttp) Error() error {
	if c.errorRaw == "" {
		return nil
	}
	return errors.New(c.errorRaw)
}

func (c *ClientHttp) setCookieData(name string, cookie *http.Cookie) {
	c.mutex.Lock()
	defer c.mutex.Unlock()

	c.cookieData[name] = cookie

}
